/*
 * Copyright (c) 2009. Silenus Consultoria, S.L.
 */
package es.silenus.detecta.hibernate;

import org.hibernate.Hibernate;
import org.hibernate.HibernateException;
import org.hibernate.MappingException;

import org.hibernate.usertype.ParameterizedType;
import org.hibernate.usertype.UserType;

import java.io.Serializable;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;

import java.util.Properties;


/**
 * Custom enumeration to integer column Hibernate type.
 *
 * @author <a href="mailto:malonso@silenus-consultoria.es">Mariano Alonso</a>
 *
 * @since 10-jun-2009 13:31:53
 */
public class CustomIntEnum implements UserType, ParameterizedType {
	/**
	 * The enum class.
	 */
	private Class<Enum> enumClass;

	/**
	 * The build method.
	 */
	private Method buildMethod;

	/**
	 * The get method.
	 */
	private Method getMethod;

	/**
	 * Return the SQL type codes for the columns mapped by this type. The codes are defined on
	 * <tt>java.sql.Types</tt>.
	 *
	 * @return int[] the typecodes
	 *
	 * @see java.sql.Types
	 */
	public int[] sqlTypes() {
		return new int[] { Hibernate.INTEGER.sqlType() };
	}

	/**
	 * The class returned by <tt>nullSafeGet()</tt>.
	 *
	 * @return Class
	 */
	public Class returnedClass() {
		return enumClass;
	}

	/**
	 * Compare two instances of the class mapped by this type for persistence "equality". Equality of the
	 * persistent state.
	 *
	 * @param x x object.
	 * @param y y object.
	 *
	 * @return boolean
	 *
	 * @throws HibernateException if something goes wrong.
	 */
	public boolean equals(Object x, Object y) throws HibernateException {
		return (x == y) || (!((null == x) || (null == y)) && x.equals(y));
	}

	/**
	 * Get a hashcode for the instance, consistent with persistence "equality"
	 *
	 * @param x the object.
	 *
	 * @return the hash code.
	 *
	 * @throws HibernateException if something goes wrong.
	 */
	public int hashCode(Object x) throws HibernateException {
		return x.hashCode();
	}

	/**
	 * Retrieve an instance of the mapped class from a JDBC resultset. Implementors should handle possibility of
	 * null values.
	 *
	 * @param rs a JDBC result set
	 * @param names the column names
	 * @param owner the containing entity
	 *
	 * @return Object
	 *
	 * @throws HibernateException
	 * @throws SQLException
	 */
	public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws HibernateException, SQLException {
		final Integer name = rs.getInt(names[0]);
		Object result = null;

		if(!rs.wasNull()) {
			try {
				result = buildMethod.invoke(null, name);
			} catch(IllegalAccessException e) {
				throw new HibernateException("Method " + buildMethod.toGenericString() + " could not be accessed", e);
			} catch(InvocationTargetException e) {
				throw new HibernateException("Method " + buildMethod.toGenericString() + " invocation failed", e);
			}
		}

		return result;
	}

	/**
	 * Write an instance of the mapped class to a prepared statement. Implementors should handle possibility of
	 * null values. A multi-column type should be written to parameters starting from <tt>index</tt>.
	 *
	 * @param st a JDBC prepared statement
	 * @param value the object to write
	 * @param index statement parameter index
	 *
	 * @throws HibernateException
	 * @throws SQLException
	 */
	public void nullSafeSet(PreparedStatement st, Object value, int index) throws HibernateException, SQLException {
		if(null == value) {
			st.setNull(index, Types.INTEGER);
		} else {
			Integer result;

			try {
				result = (Integer)getMethod.invoke(value);
			} catch(IllegalAccessException e) {
				throw new HibernateException("Method " + getMethod.toGenericString() + " could not be accessed", e);
			} catch(InvocationTargetException e) {
				throw new HibernateException("Method " + getMethod.toGenericString() + " invocation failed", e);
			}

			st.setInt(index, result);
		}
	}

	/**
	 * Return a deep copy of the persistent state, stopping at entities and at collections. It is not necessary to
	 * copy immutable objects, or null values, in which case it is safe to simply return the argument.
	 *
	 * @param value the object to be cloned, which may be null
	 *
	 * @return Object a copy
	 *
	 * @throws HibernateException if something goes wrong.
	 */
	public Object deepCopy(Object value) throws HibernateException {
		return value;
	}

	/**
	 * Are objects of this type mutable?
	 *
	 * @return boolean
	 */
	public boolean isMutable() {
		return false;
	}

	/**
	 * Transform the object into its cacheable representation. At the very least this method should perform a deep
	 * copy if the type is mutable. That may not be enough for some implementations, however; for example, associations
	 * must be cached as identifier values. (optional operation)
	 *
	 * @param value the object to be cached
	 *
	 * @return a cachable representation of the object
	 *
	 * @throws HibernateException
	 */
	public Serializable disassemble(Object value) throws HibernateException {
		return (Serializable)value;
	}

	/**
	 * Reconstruct an object from the cacheable representation. At the very least this method should perform a deep
	 * copy if the type is mutable. (optional operation)
	 *
	 * @param cached the object to be cached
	 * @param owner the owner of the cached object
	 *
	 * @return a reconstructed object from the cachable representation
	 *
	 * @throws HibernateException
	 */
	public Object assemble(Serializable cached, Object owner) throws HibernateException {
		return cached;
	}

	/**
	 * During merge, replace the existing (target) value in the entity we are merging to with a new (original)
	 * value from the detached entity we are merging. For immutable objects, or null values, it is safe to simply return
	 * the first parameter. For mutable objects, it is safe to return a copy of the first parameter. For objects with
	 * component values, it might make sense to recursively replace component values.
	 *
	 * @param original the value from the detached entity being merged
	 * @param target the value in the managed entity
	 * @param owner the owner.
	 *
	 * @return the value to be merged
	 *
	 * @throws HibernateException if something goes wrong.
	 */
	public Object replace(Object original, Object target, Object owner) throws HibernateException {
		return original;
	}

	/**
	 * Gets called by Hibernate to pass the configured type parameters to the implementation.
	 *
	 * @param parameters the parameters.
	 *
	 * @throws MappingException if something goes wrong.
	 */
	@SuppressWarnings({"unchecked"})
	public void setParameterValues(Properties parameters) {
		final String enumClassName = parameters.getProperty("enumClassName");

		if(enumClassName == null) {
			throw new MappingException("enumClassName parameter not specified");
		}

		String buildMethodName = parameters.getProperty("buildMethod");

		if(buildMethodName == null) {
			buildMethodName = "getById";
		}

		String getMethodName = parameters.getProperty("getMethod");

		if(getMethodName == null) {
			getMethodName = "getId";
		}

		try {
			this.enumClass = (Class<Enum>)Class.forName(enumClassName);

			this.buildMethod = this.enumClass.getMethod(buildMethodName, int.class);

			this.getMethod = this.enumClass.getMethod(getMethodName);
		} catch(ClassNotFoundException e) {
			throw new MappingException("enumClass " + enumClassName + " not found", e);
		} catch(NoSuchMethodException e) {
			throw new MappingException(
				"enumClass " + enumClassName + " must inplement a buildMethod: public static Enum " + buildMethodName
				+ "(int type);", e);
		}
	}
}
